Genesis 3D

 

per collaborazioni, commenti, critiche, e altro contattateci alla e-mail: clubinfo@libero.it risponderemo al più presto!

Corso sull'engine 3D di Genesis

Undicesima lezione

di Luca Sabatucci

Per commentare, dare suggerimenti o consigli (molto apprezzati) e per segnalare eventuali errori o
disattenzioni (sempre possibili), puoi inviare una email all'autore (clicca sul nome sopra). Grazie per la collaborazione!


Programmazione: parte II

In questo tutorial vedremo come:

1) rilevare pressione dei tasti e movimento del mouse

2) muovere la telecamera attorno all'ambiente

3) rilevare le collisioni

4) applicare la gravità

 

Le spiegazioni saranno fornite in modo incrementale rispetto al primo tutorial. Forniremo quindi sole li informazioni aggiuntive e non l'intero codice.

Tastiera

Il primo posto nella quale la pressione dei tasti viene rilevata è la funzione WndProc, richiamata da windows stessa quando ci sono dei messaggi in attesa. Tale funzione modifica lo stato dell'elemento di un array corrispondente al tasto premuto. TRUE = premuto, FALSE = rilasciato.

 

Tale array si trova nella struttura InputOutput e viene istanziato nella variabile io. Quindi per accedere, ad esempio, alla freccia in su della tastiera si deve leggere il valore di io.keys[VK_UP]. La costante VK_UP viene definita in Winuser.h; ne diamo un piccolo esempio:

 

#define VK_SPACE          0x20

#define VK_PRIOR          0x21

#define VK_NEXT           0x22

#define VK_END            0x23

#define VK_HOME           0x24

#define VK_LEFT           0x25

#define VK_UP             0x26

#define VK_RIGHT          0x27

#define VK_DOWN           0x28

#define VK_SELECT         0x29

#define VK_PRINT          0x2A

#define VK_EXECUTE        0x2B

#define VK_SNAPSHOT       0x2C

#define VK_INSERT         0x2D

#define VK_DELETE         0x2E

#define VK_HELP           0x2F

Mouse

Mentre la pressione dei tasti ci serve per muovere la telecamera avanti e indietro rispetto ad una posizione iniziale e un vettore di orientamento, il mouse può essere utile per direzionare tale vettore. Movendo a destra e a sinistra il mouse il vettore di orientamento viene ruotato in senso orario e antiorario.

 

Definiamo nella main.h la costante sensibilità del mouse:

 

#define     MOUSESENSITIVITY  0.002f;

 

Questo valore potremmo in una versione successiva leggerlo direttamente da file.

Per leggere la posizione del mouse utilizzeremo la funzione GetCursorPos(POINT *posizione) che fa parte delle API di windows. Dopo la chiamata posizione contiene le coordinate schermo del puntatore del mouse.

Per rilevare i movimenti relativi del mouse prima poliamo il cursore al centro dello schermo, poi leggiamo la posizione e calcoliamo la differenza in termini di deltaX e deltaY. A questo punto moltiplichiamo deltaX e deltaY per la sensitività del mouse e otteniamo lo spostamento relativo nelle due dimensioni.

 

Adesso vedremo come utilizzare questi dati.

Movimento della telecamera

Associamo alla telecamera due vettori. Il primo vettore indica la posizione della telecamera nello spazio globale. In realtà la telecamera la solleviamo rispetto a tale posizione di un certo valore che rappresenta la nostra altezza virtuale. Quindi il vettore posizione indica la posizione dei piedi e non della telecamera.

Il secondo vettore è l'angolo, ovvero l'orientamento della telecamera. Va immaginato applicato al punto in cui giace la telecamera e indica la direzione dello sguardo. Utilizzare un vettore, anziché un semplice valore float ci permette di avere 2 gradi di libertà (destra-sinistra e alto basso).

Avevamo già definito la struttura Player come

 

typedef struct {

      geVec3d     posizione;        // Posizione del giocatore

      geVec3d     angolo;           // direzione sguardo

 

      geVec3d     Mins;             // Bounding box

      geVec3d     Maxs;            

 

      int   height;                 //how tall are we

      int   speed;                  //how fast are we

      int   caduta;                 // velocità di caduta

      int   salto;                 // altezza del salto

      int   gradino;               // altezza massima del gradino che può salire

 

      int   stato;

      int   counter;

} Player;

 

I due vettori di cui parlo sono posizione e angolo.

 

Iniziamo con ordine, analizzando la funzione WinMain(). In marrone segno le differenze e le aggiunte rispetto alla prima versione.

 

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd)

{

      LoadPrefs(io.driver, &io.Width, &io.Height, io.map);

 

      CreateMainWindow(hInstance,nShowCmd);

     

      //Initialize the Genesis engine

      InitEngine(hWnd);

      Setup();

 

      MainLoop();

 

      //Shutdown the Genesis engine and clean up the memory     

      Shutdown();

     

      //End Program

      return 1;

}

 

L'unica differenza sta nella funzione Setup() nella quale diamo un valore ai campi di player.

 

void Setup(void) {

      // posizione e orientamento inziale del personaggio

      pl.posizione.X = 0.0f;

      pl.posizione.Y = 100.0f;

      pl.posizione.Z = 0.0f;

 

      pl.angolo.X = 0.0f;

      pl.angolo.Y = 0.0f;

      pl.angolo.Z = 0.0f;

 

      // impostazione del bounding box attorno al personaggio

      pl.Mins.X = -20.0f;  

      pl.Mins.Y = 0.0f;

      pl.Mins.Z = -20.0f;

      pl.Maxs.X = 20.0f;

      pl.Maxs.Y = 175.0f;

      pl.Maxs.Z = 20.0f;

     

      // We will set this values up here only for an easier transition later.

      pl.height = 170;    // this is actually our players eye level, and not real height

      pl.speed  = 4;

      pl.caduta = 6;

      pl.gradino = 41;

      pl.salto = 70;

 

      // stato inziale

      pl.stato = standing;

}

 

La posizione del player è fissata in (0,100,0) in modo che l'effetto iniziale sia quello dell'uomo che cade verso il basso. Questi valori naturalmente possono essere diversi. Il loro valore dipende dalla mappa che viene usata nell'applicazione. Scegliere un punto di inizio adeguato.

 

L'angolo setta la direzione dello sguardo all'inizio.

 

Mins e Maxs verranno spiegato dopo. Le altre variabili rappresentando dei parametri come la velocità, l'altezza, ecc...

Lo stato del player viene fissato a "standing".

La funzione MainLoop()

In questa funzione i cambiamenti sono già più sostanziali.

 

void MainLoop() {

      MSG         msg;

      int         run;

      geBoolean   coll;

 

      //Main game loop

      run = 1;

 

      while (run)

      {

            if (PeekMessage(&msg,NULL,0,0,PM_REMOVE)) {    // Is There A Message Waiting?

                  if (msg.message==WM_QUIT) {                    // Have We Received A Quit Message?

                        run = 0;                                       // If So done=TRUE

                  } else {                                             // If Not, Deal With Window Messages

                        TranslateMessage(&msg);                  // Translate The Message

                        DispatchMessage(&msg);                   // Dispatch The Message

                  }

            } else {                                                   // If There Are No Messages

 

                  if (pl.stato == standing || pl.stato == falling) {

                        coll = Gravity((float)pl.caduta);

                        if (coll == GE_TRUE)

                             pl.stato = standing;

                  }

 

                  // Aggiorna l'ambiente

                  MoveCamera();

                  AggiornaXForm();  // sposta la camera

 

Prima di fare il rendering controlliamo lo stato del player. Se esso è "standing" oppure "falling" allora applichiamo la gravità.

La funzione gravity fa esattamente quello che ci si aspetta: verifica se il player si trova su qualche cosa, altrimenti lo fa "cadere" di una certa quantità (in relazione alle leggi di gravitazione e al buon senso). Se il player atterra su qualche cosa allora restituisce TRUE.

MoveCamera aggiorna la posizione del player in base all'input da tastiera e mouse.

AggiornaXForm applica le trasformazioni alla telecamera.

 

                  if (!geEngine_BeginFrame(Engine, Camera, GE_FALSE))

                        run = 0;

 

                  if (!geEngine_RenderWorld(Engine, World, Camera, 0.0f))

                        run = 0;

 

                  if (!geEngine_EndFrame(Engine))

                        run = 0;

 

                  if (io.keys[VK_ESCAPE]) {                                 

                        run=0;

                  }

            }

      }

}

La funzione MoveCamera()

Ecco la nuova funzione MoveCamera, usata per aggiornare la posizione del player in base alla tastiera e al mouse.

 

void MoveCamera(void) {

      float forwardmove;      // movimento in avanti

      geVec3d angolo;         // angolo testa

      geVec3d normale;

      geVec3d newpos;

      geBoolean coll = GE_FALSE;

     

      angolo = MoveHead();                                             // controlla il mouse

      pl.angolo.Y += angolo.Y;

      pl.angolo.X += angolo.X;

      //make sure we arent looking too far up or down. If we are then fix that!

      if (pl.angolo.X >0.9f)  pl.angolo.X =0.9f;

      if (pl.angolo.X <-0.9f) pl.angolo.X =-0.9f;

 

La prima operazione da fare è verificare lo spostamento del mouse attraverso la funzione MoveHead(). Essa restituisce un vettore che rappresenta lo spostamento angolare relativo nelle direzioni X (sopra-sotto) e Y (destra-sinistra).

Naturalmente imponiamo dei limiti all'angolo X in modo che lo sguardo non possa andare oltre un certo range.

 

      if (pl.stato == standing) {

            //pl.posizione.Y += pl.gradino;

           

            forwardmove = CheckKeyboard();                                   // controlla la tastiera

           

            normale = Player_Normal(FRONT);   

            geVec3d_AddScaled(&pl.posizione, &normale, forwardmove, &newpos);

 

            pl.posizione=ControllaCollisione(pl.posizione,newpos,&coll);           // verifica le collisioni

 

Secondo passo è quello di verificare la pressione dei tasti freccia in alto, freccia in basso mediante la funzione CheckKeyboard() che restituisce la "quantità di spostamento" da effettuare. Un valore negativo indica un movimento verso dietro.

Quindi viene rilevato il vettore frontale al player (che naturalmente dipende dall'angolo) e il vettore posizione viene aggiornato sommando ad esso il vettore dello spostamento.

La funzione AddScaled effettua questa operazione newpos = pl.posizione + (normale * forwardmove).

Poi viene verificata la collisione con eventuali oggetti (muri, porte...), ma ce ne occuperemo dopo.

 

            if (coll) {

                  pl.posizione.Y += pl.gradino;

                 

                  forwardmove = CheckKeyboard();                                   // controlla la tastiera

                 

                  normale = Player_Normal(FRONT);   

                  geVec3d_AddScaled(&pl.posizione, &normale, forwardmove, &newpos);

 

                  pl.posizione=ControllaCollisione(pl.posizione,newpos,&coll);           // verifica le collisioni

                  Gravity((float)pl.gradino);  // usata così questa funzione serve a muovere il giocatore verso il basso

                                                           // di un certo valore fino a fargli toccare il terreno

            }

      }

}

 

Anche questa parte è dedicata alle collisioni; il suo scopo è quello di salire i gradini. Il problema essenziale dei gradini è che quando ci ha una collisione con un gradino, ci deve essere un metodo per verificare che si tratta di un ostacolo superabile, mentre un muro non lo è. La soluzione riportata (è chiaro che ce ne possono essere delle altre) è la seguente. Qualora ci sia una collisione, si tenta di andare ugualmente nella direzione, ma salendo la posizione di un valore pl.gradino, che rappresenta il massimo dislivello che si può superare. Se la collisione persiste, allora si tratta di un muro, altrimenti vuol dire che si tratta di un dislivello superabile e si applica la funzione gravità per arrivare a "toccare" la superficie superiore del gradino stesso.

La funzione MoveHead()

Il comportamento di tale funzione è stata già accennata quando si è parlato di mouse.

 

geVec3d MoveHead(void)             //Moves the camera base on our mouse position

{

      POINT temppos;

      geVec3d     TempAngles;

      geFloat TURN_SPEED;                      // speed camera will move if turning left/right

      geFloat UPDOWN_SPEED;              // speed camera will move if looking up/down

      int screen_x,screen_y;

      int tempscreen_x,tempscreen_y;

 

      TempAngles.X = 0.0f;

      TempAngles.Y = 0.0f;

      TempAngles.Z = 0.0f;

 

      GetCursorPos(&temppos); // get the mouse position in SCREEN coordinates

      tempscreen_x=temppos.x;

      tempscreen_y=temppos.y;

 

      screen_x = io.Width/2;             // calculate the center of the screen

      screen_y = io.Height/2;            // calculate the center of the screen

      SetCursorPos(screen_x, screen_y);  // set the cursor in the center of the screen

      TURN_SPEED   = abs(tempscreen_x-screen_x) * MOUSESENSITIVITY;    // calculate the turning speed

      UPDOWN_SPEED = abs(tempscreen_y-screen_y) * MOUSESENSITIVITY;    // calculate the up/down speed

 

      if ((tempscreen_x != screen_x) || (tempscreen_y != screen_y))

      {

            if (tempscreen_x > screen_x) // is it to the left?

                  TempAngles.Y = -(geFloat)(TURN_SPEED); //if so spin left

            else if (tempscreen_x < screen_x) // is it to the right?

                  TempAngles.Y = (geFloat)(TURN_SPEED); //if so spin right

            if (tempscreen_y > screen_y) // is it to the top?

                  TempAngles.X = -(geFloat)(UPDOWN_SPEED); //if so look up        

            else if (tempscreen_y < screen_y) // is it to the bottom?

                  TempAngles.X = (geFloat)(UPDOWN_SPEED); //if so look down 

      }

 

      return TempAngles;

}

La funzione CheckKeyboard()

Funzione piuttosto semplice: verifica lo stato dei tasti e determina lo spostamento in base alla velocità del player.

 

float CheckKeyboard() {

      int speed = pl.speed;

      float forwardspeed=0;

     

      if (io.keys[VK_LBUTTON] || io.keys[VK_CONTROL])

            speed = pl.speed*2;

 

      if (io.keys[VK_UP])                                 

            forwardspeed += speed;

 

      if (io.keys[VK_DOWN])                         

            forwardspeed -= pl.speed;

 

      if (io.keys[VK_SPACE])

            if (pl.stato == standing)

                  player_jump_start();

     

      return forwardspeed;

}

 

Qualora venga premuto il tasto CTRL la velocità viene raddoppiata simulando l'effetto corsa. La funzione restituisce lo spostamento.

La funzione Player_Normal()

Tale funzione riceve in ingresso la direzione di cui si vuole il versore. Le possibilità sono: UP, FRONT, LEFT. Prendiamo ad esempio la direzione FRONT; la funzione restituisce la proiezione dell'angolo rispetto al piano X-Z.

 

 

geVec3d Player_Normal(int direction) {

      // usa la variabile globale pl per determinare la direzione "basso"

      geXForm3d XForm;

      geVec3d normale;

 

      geXForm3d_SetIdentity(&XForm);

      geXForm3d_RotateY(&XForm,pl.angolo.Y);

 

      switch (direction) {

      case UP:

            geXForm3d_GetUp(&XForm,&normale);

            break;

      case FRONT:

            geXForm3d_GetIn(&XForm,&normale);

            break;

      case LEFT:

            geXForm3d_GetLeft(&XForm,&normale);

            break;

      }

 

      return normale;

}

 

Questo viene sostanzialmente effettuato mediante la funzione geXForm3d_Getxx dove al posto di xx si mette Up, In o Left.

La funzione AggiornaXForm()

Costruisce la matrice di trasformazione in base a io.posizione e io.angolo. Tiene conto dell'altezza del player e posiziona la camera nello spazio globale mediante la matrice così costruita.

 

void AggiornaXForm() {

      geXForm3d ViewXForm;

 

      geXForm3d_SetIdentity(&ViewXForm);

 

      //Setup the rotation

      geXForm3d_RotateX(&ViewXForm, pl.angolo.X);

      geXForm3d_RotateY(&ViewXForm, pl.angolo.Y);

      geXForm3d_RotateZ(&ViewXForm, pl.angolo.Z);

 

      geXForm3d_Translate(&ViewXForm, pl.posizione.X-100, pl.posizione.Y+pl.height, pl.posizione.Z);

 

      //Set the XForm to the camera

      geCamera_SetWorldSpaceXForm(Camera, &ViewXForm);

}

Le collisioni

Durante il movimento della telecamera nell'ambiente risulta indispensabile effettuare il controllo delle collisioni con gli elementi presenti nell'ambiente stesso: muri, scalini ecc...

Il controllo delle collisioni è demandato all'engine mediante una sola funzione: geWorld_Collision, la cui sintassi d'uso è (dalla documentazione originale)

 

GENESISAPI geBoolean geWorld_Collision(geWorld *World, const geVec3d *Mins, const geVec3d *Maxs, const geVec3d *Front, const geVec3d *Back, uint32 Contents, uint32 CollideFlags, uint32 UserFlags, GE_CollisionCB *CollisionCB, void *Context, GE_Collision *Collision);

 

dove:

World è il puntatore all'oggetto geWorld con il quale si vuole determinare la collisione.

Mins e Maxs rappresentano i vertici che definiscono un parallelepipedo. La collisione verrà determinata tra l'area del parallelepipedo e gli oggetti presenti nell'ambiente. Le coordinate del parallelepipedo hanno come riferimento uno spazio di stato locale. Esse devono riferirsi come spazio di stato globale ad altri due parametri: front e back.

Front e Back rappresentano due parametri con una duplice funzione. Possono essere usati per una collisione "lineare" anziché "volumetrica" oppure possono essere usati per fornire un riferimento di coordinate ai parametri Mins e Maxs.

Contents è una combinazione di alcune costanti definite in genesis.h

 

#define GE_CONTENTS_SOLID (1<<0)          // Solid (Visible)

#define GE_CONTENTS_WINDOW (1<<1)         // Window (Visible)

#define GE_CONTENTS_EMPTY (1<<2)          // Empty but Visible (water, lava, etc...)

 

#define GE_CONTENTS_TRANSLUCENT (1<<3)    // Vis will see through it

#define GE_CONTENTS_WAVY (1<<4)           // Wavy (Visible)

#define GE_CONTENTS_DETAIL (1<<5)         // Won't be included in vis oclusion

 

#define GE_CONTENTS_CLIP (1<<6)           // Structural but not visible

#define GE_CONTENTS_HINT (1<<7)           // Primary splitter (Non-Visible)

#define GE_CONTENTS_AREA (1<<8)           // Area seperator leaf (Non-Visible)

 

// These contents are all solid types

#define GE_CONTENTS_SOLID_CLIP (GE_CONTENTS_SOLID | GE_CONTENTS_WINDOW | GE_CONTENTS_CLIP)

#define GE_CONTENTS_CANNOT_OCCUPY GE_CONTENTS_SOLID_CLIP

 

// These contents are all visible types

#define GE_VISIBLE_CONTENTS (GE_CONTENTS_SOLID | GE_CONTENTS_EMPTY | GE_CONTENTS_WINDOW | GE_CONTENTS_WAVY)

 

Lo scopo è quello di specificare il tipo di oggetti con le quali è possibile avere delle collisioni. Generalmente la scelta migliore è GE_CONTENTS_SOLID_CLIP che include oggetti solidi opachi e oggetti solidi trasparenti. La costante GE_CONTENTS_EMPTY specifica che si possono avere collisioni con oggetti empty, come ad esempio l'acqua, se si vogliono creare effetti particolari come il galleggiamento.

 

CollideFlags è una combinazione di

 

#define GE_COLLIDE_MESHES (1<<0)

#define GE_COLLIDE_MODELS (1<<1)

#define GE_COLLIDE_ACTORS (1<<2)

#define GE_COLLIDE_NO_SUB_MODELS (1<<3)

#define GE_COLLIDE_ALL (GE_COLLIDE_MESHES | GE_COLLIDE_MODELS | GE_COLLIDE_ACTORS)

 

Queste costanti servono a specificare con che cosa si può avere la collisione. GE_COLLIDE_MESHES indica solo oggetti statici della mappa. GE_COLLIDE_MODELS include gli oggetti dinamici della mappa (porte che si aprono). GE_COLLIDE_ACTORS indica che anche gli actor vengono inclusi nella collisione.

 

UserFlag è una "maschera" che può essere utile per limitare la collisione ad una classe di actor. Si vedrà in seguito che in fase di creazione dell'actor si deve specificare anche lì un valore di mask. Se con un'operazione di OR logico il valore è true allora la collisione viene effettuata. Viene usato per definire alcuni actor con i quali non ci deve essere collisione.

 

CollisionCB e Context sono due parametri usati per richiamare una funzione di callback. Qualora i flag messi a disposizione non siano sufficienti per definire il tipo di collisione richiesta, allora si può fare in modo che il controllo venga eseguito da una funzione utente. Il puntatore a tale funzione è proprio CollisionCB, mentre Context è un puntatore agli argomenti da passare a tale funzione. Qualora la funzione restituisca GE_TRUE allora la collisione è accettata, altrimenti viene rifiutata.

 

Collision è un puntatore ad una struttura di tipo GE_Collision

 

typedef struct{

    geWorld_Model *Model;       // Pointer to what model was hit (if any)

    geMesh *Mesh;               // Pointer to what mesh was hit (if any)

    geActor *Actor;             // Pointer to what actor was hit (if any)

    geVec3d Impact;             // Impact Point

    float Ratio;                // Percent from 0 to 1.0, how far along the line for the impact point

    GE_Plane Plane;             // Impact Plane

} GE_Collision;

 

se viene rilevata una collisione la struttura viene modificata con i valori corretti. Model, Mesh e Actor conterranno un puntatore all'oggetto con il quale si ha avuto la collisione. Impact fornisce il punto dello spazio nel quale è avvenuta la collisione. Plane fornisce il piano con il quale è avvenuto l'impatto. Può essere utile se si vogliono creare effetti di scivolamento.

La funzione ControllaCollisione()

Dopo tutte queste spiegazioni, capire la funzione che controlla le collisioni nel nostro programma sarà molto semplice.

 

geVec3d ControllaCollisione(geVec3d oldpos,geVec3d newpos,geBoolean *coll) {

      GE_Collision Collision;

      BOOL result;

 

    result=geWorld_Collision(World,

                             &pl.Mins,

                             &pl.Maxs,

                             &oldpos,

                             &newpos,

                             GE_CONTENTS_SOLID,

                             GE_COLLIDE_ALL,

                             256,

                             NULL,

                             NULL,

                             &Collision); //checks for collision

 

      if (coll)

            if (Collision.Model || Collision.Mesh)

                  *coll = result;

 

    if (result==1)

            return Collision.Impact;

      else

            return newpos;

     

}

 

Tale funzione è una estensione della geWorld_Collision, in quanto dati due punti nello spazio (la vecchia posizione e la nuova posizione) la funzione restituisce due valori: un punto (che può essere il nuovo punto se non è avvenuta la collisione o il punto di impatto) e un booleano che indica se la collisione è avvenuta o meno.

La funzione Gravity()

La funzione gravity svolge il ruolo di far cadere il player se si trova "in aria". La funzione tenta uno spostamento verso il basso e controlla la collisione. Se avviene abbiamo raggiunto la posizione "a terra", altrimenti effettua lo spostamento.

Tale spostamento è proporzionale alla variabile speed che indica la velocità di caduta per frame.

 

geBoolean Gravity(geFloat speed) {

      geFloat caduta = -1.0f * speed;

      geVec3d normale;

      geVec3d newpos;

      geBoolean collisione = GE_FALSE;

 

      normale = Player_Normal(UP);

      geVec3d_AddScaled(&pl.posizione, &normale, caduta, &newpos);

      pl.posizione=ControllaCollisione(pl.posizione,newpos,&collisione);     // verifica la collisione col suolo

     

      return collisione;

}

 

Nel prossimo tutorial vedremo come aggiungere degli effetti dinamici all'ambiente, movendo Modelli e Actor e inserendo degli effetti sonori.

 

Questo articolo è stato scaricato dal Club di informatica
Pagina curata da:
Luca Sabatucci